﻿using System;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace LongNet.Spider
{
    /// <summary>
    /// Information about a downloaded resource.
    /// Additionally with information about local resources.
    /// </summary>
    [Serializable]
    public sealed class DownloadedResourceInformation :
        UriResourceInformation,
        IEquatable<DownloadedResourceInformation>
    {
        #region Public methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="copyFrom">The copy from.</param>
        public DownloadedResourceInformation(
            DownloadedResourceInformation copyFrom)
            :
            base(copyFrom)
        {
            _localBaseFolderPath = copyFrom._localBaseFolderPath;
            _localFolderPath = copyFrom._localFolderPath;
            _localFilePath = copyFrom._localFilePath;
            _localFileName = copyFrom._localFileName;

            _addedByProcessID = copyFrom._addedByProcessID;
            _dateAdded = copyFrom._dateAdded;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="copyFrom">The copy from.</param>
        /// <param name="baseFolderPath">The base folder path.</param>
        public DownloadedResourceInformation(
            UriResourceInformation copyFrom,
            DirectoryInfo baseFolderPath)
            :
            base(copyFrom)
        {
            _localBaseFolderPath = baseFolderPath;

            _localFilePath = new FileInfo(
                Path.Combine(
                    baseFolderPath.FullName,
                    MakeLocalFileName(
                        copyFrom.AbsoluteUri,
                        copyFrom.BaseUri,
                        copyFrom.LinkType).Name));

            _localFileName =
                new FileInfo(_localFilePath.Name);
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="copyFrom">The copy from.</param>
        /// <param name="folderPath">The folder path.</param>
        /// <param name="baseFolderPath">The base folder path.</param>
        public DownloadedResourceInformation(
            UriResourceInformation copyFrom,
            DirectoryInfo folderPath,
            DirectoryInfo baseFolderPath)
            :
            base(copyFrom)
        {
            _localFolderPath = folderPath;
            _localBaseFolderPath = baseFolderPath;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="options">The options.</param>
        /// <param name="originalUrl">The original URL.</param>
        /// <param name="uri">The URI.</param>
        /// <param name="baseUri">The base URI.</param>
        /// <param name="folderPath">The folder path.</param>
        /// <param name="baseFolderPath">The base folder path.</param>
        /// <param name="linkType">Type of the link.</param>
        public DownloadedResourceInformation(
            WebSiteDownloaderOptions options,
            string originalUrl,
            Uri uri,
            Uri baseUri,
            DirectoryInfo folderPath,
            DirectoryInfo baseFolderPath,
            UriType linkType)
            :
            base(options, originalUrl, uri, baseUri, linkType)
        {
            _localFolderPath = folderPath;
            _localBaseFolderPath = baseFolderPath;
        }

        // ------------------------------------------------------------------
        #endregion

        #region Public properties.
        // ------------------------------------------------------------------

        /// <summary>
        /// Gets the local base folder path.
        /// </summary>
        /// <value>The local base folder path.</value>
        public DirectoryInfo LocalBaseFolderPath
        {
            get
            {
                return _localBaseFolderPath;
            }
        }

        /// <summary>
        /// Gets the local folder path.
        /// </summary>
        /// <value>The local folder path.</value>
        public DirectoryInfo LocalFolderPath
        {
            get
            {
                return _localFolderPath;
            }
        }

        /// <summary>
        /// Gets the local file path.
        /// </summary>
        /// <value>The local file path.</value>
        public FileInfo LocalFilePath
        {
            get
            {
                return _localFilePath;
            }
        }

        /// <summary>
        /// Gets the name of the local file.
        /// </summary>
        /// <value>The name of the local file.</value>
        public FileInfo LocalFileName
        {
            get
            {
                return _localFileName;
            }
        }

        /// <summary>
        /// Gets the added by process ID.
        /// </summary>
        /// <value>The added by process ID.</value>
        public int AddedByProcessID
        {
            get
            {
                return _addedByProcessID;
            }
        }

        /// <summary>
        /// Gets the date added.
        /// </summary>
        /// <value>The date added.</value>
        public DateTime DateAdded
        {
            get
            {
                return _dateAdded;
            }
        }

        // ------------------------------------------------------------------
        #endregion

        #region Private methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Makes the name of the local file.
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <param name="baseUri">The base URI.</param>
        /// <param name="linkType">Type of the link.</param>
        /// <returns></returns>
        private static FileInfo MakeLocalFileName(
            Uri uri,
            Uri baseUri,
            UriType linkType)
        {
            Debug.Assert(
                uri.IsAbsoluteUri,
                @"URI must be absolute but is not.");

            // Each URI is unique, use this fact.
            string unique =
                uri.AbsoluteUri.GetHashCode().ToString(@"X8");

            FileInfo result;

            if (linkType == UriType.Content)
            {
                if (string.Compare(
                    uri.AbsoluteUri.TrimEnd('/'),
                    baseUri.AbsoluteUri.TrimEnd('/'),
                    true) == 0)
                {
                    result = new FileInfo(@"index.html");
                }
                else
                {
                    result = new FileInfo(unique +
                        CorrectFileExtension(
                            TryExtractFileExtension(uri)));
                }
            }
            else
            {
                Debug.Assert(linkType == UriType.Resource);

                result =
                    new FileInfo(unique +
                        CorrectFileExtension(
                            TryExtractFileExtension(uri)));
            }

            Trace.WriteLine(
                string.Format(
                    @"Making local file path '{0}' for URI '{1}'.",
                    result.FullName,
                    uri.AbsoluteUri));

            return result;
        }

        /// <summary>
        /// Corrects the file extension.
        /// </summary>
        /// <param name="extension">The extension.</param>
        /// <returns></returns>
        private static string CorrectFileExtension(
            string extension)
        {
            if (string.IsNullOrEmpty(extension))
            {
                return extension;
            }
            else
            {
                extension = extension.TrimStart('.').ToLowerInvariant();

                foreach (string allowedExtension in _allowedExtensions)
                {
                    if (allowedExtension == extension)
                    {
                        return @"." + extension;
                    }
                }

                return @".html";
            }
        }

        /// <summary>
        /// 
        /// </summary>
        private static readonly string[] _allowedExtensions =
            new string[] 
			{ 
				@"jpg",
				@"jpeg",
				@"png",
				@"gif",
				@"html",
				@"css",
				@"pdf",
				@"txt",
			};

        /// <summary>
        /// Tries the extract file extension.
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <returns></returns>
        private static string TryExtractFileExtension(
            Uri uri)
        {
            string ext = Path.GetExtension(
                uri.AbsoluteUri.Split('?')[0]);

            return ext;
        }

        /// <summary>
        /// Gets a value indicating whether [file exists].
        /// </summary>
        /// <value><c>true</c> if [file exists]; otherwise, <c>false</c>.</value>
        public bool FileExists
        {
            get
            {
                FileInfo fileName = MakeLocalFileName(
                    AbsoluteUri,
                    BaseUri,
                    LinkType);

                FileInfo filePath = new FileInfo(
                    Path.Combine(
                        _localFolderPath.FullName,
                        fileName.Name));

                return filePath.Exists;
            }
        }

        // ------------------------------------------------------------------
        #endregion

        #region Private variables.
        // ------------------------------------------------------------------

        private readonly DirectoryInfo _localBaseFolderPath;
        private readonly DirectoryInfo _localFolderPath;
        private readonly FileInfo _localFilePath;
        private readonly FileInfo _localFileName;

        private readonly int _addedByProcessID = Process.GetCurrentProcess().Id;
        private readonly DateTime _dateAdded = DateTime.Now;

        // ------------------------------------------------------------------
        #endregion

        #region IEquatable<DownloadedResourceInformation> members.
        // ------------------------------------------------------------------

        /// <summary>
        /// Indicates whether the current object is equal to another object of 
        /// the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the other parameter; 
        /// otherwise, false.
        /// </returns>
        public bool Equals(
            DownloadedResourceInformation other)
        {
            if (AbsoluteUri == null && other.AbsoluteUri == null)
            {
                return true;
            }
            else if (AbsoluteUri == null || other.AbsoluteUri == null)
            {
                return false;
            }
            else
            {
                int result =
                    string.Compare(
                        AbsoluteUri.AbsoluteUri,
                        other.AbsoluteUri.AbsoluteUri,
                        true);

                return result == 0;
            }
        }

        // ------------------------------------------------------------------
        #endregion
    }


    /// <summary>
    /// Information about a downloaded resource.
    /// </summary>
    [Serializable]
    [DebuggerDisplay(@"URI = {_absoluteUri}")]
    public class UriResourceInformation
    {
        #region Public methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="UriResourceInformation"/> class.
        /// </summary>
        /// <param name="options">The options.</param>
        /// <param name="originalUrl">The original URL.</param>
        /// <param name="uri">The URI.</param>
        /// <param name="baseUri">The base URI.</param>
        /// <param name="linkType">Type of the link.</param>
        public UriResourceInformation(
            WebSiteDownloaderOptions options,
            string originalUrl,
            Uri uri,
            Uri baseUri,
            UriType linkType)
        {
            _options = options;
            _originalUrl = originalUrl;
            _baseUri = baseUri;

            uri = new Uri(CleanupUrl(uri.OriginalString), UriKind.RelativeOrAbsolute);

            if (Uri.IsWellFormedUriString(uri.OriginalString, UriKind.Absolute))
            {
                _absoluteUri = uri;
                _relativeUri = null;
            }
            else if (Uri.IsWellFormedUriString(uri.OriginalString, UriKind.Relative))
            {
                _absoluteUri = MakeAbsoluteUri(baseUri, uri);
                _relativeUri = uri;
            }
            else
            {
                if (originalUrl.StartsWith(@"#"))
                {
                    _absoluteUri = null;
                    _relativeUri = new Uri(originalUrl, UriKind.RelativeOrAbsolute);
                }
                else
                {
                    _absoluteUri = MakeAbsoluteUri(baseUri, uri);
                    _relativeUri = uri;
                }
            }

            _linkType = linkType;
        }

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="UriResourceInformation"/> class.
        /// </summary>
        /// <param name="copyFrom">The copy from.</param>
        public UriResourceInformation(
            UriResourceInformation copyFrom)
        {
            _options = copyFrom._options;
            _originalUrl = copyFrom._originalUrl;
            _relativeUri = copyFrom._relativeUri;
            _baseUri = copyFrom._baseUri;
            _absoluteUri = copyFrom._absoluteUri;
            _linkType = copyFrom._linkType;
        }

        /// <summary>
        /// Determines whether [is on same site] [the specified URI].
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <returns>
        /// 	<c>true</c> if [is on same site] [the specified URI]; otherwise, 
        /// <c>false</c>.
        /// </returns>
        public bool IsOnSameSite(
            Uri uri)
        {
            if (string.Compare(_absoluteUri.Host, uri.Host, true) == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        // ------------------------------------------------------------------
        #endregion

        #region Public properties.
        // ------------------------------------------------------------------

        /// <summary>
        /// Gets the original URL.
        /// </summary>
        /// <value>The original URL.</value>
        public string OriginalUrl
        {
            get
            {
                return _originalUrl;
            }
        }

        /// <summary>
        /// Gets the relative URI.
        /// </summary>
        /// <value>The relative URI.</value>
        public Uri RelativeUri
        {
            get
            {
                return _relativeUri;
            }
        }

        /// <summary>
        /// Gets the base URI.
        /// </summary>
        /// <value>The base URI.</value>
        public Uri BaseUri
        {
            get
            {
                return _baseUri;
            }
        }

        /// <summary>
        /// Gets the base URI with folder.
        /// </summary>
        /// <value>The base URI with folder.</value>
        public Uri BaseUriWithFolder
        {
            get
            {
                if (_absoluteUri.AbsoluteUri.Contains(@"/"))
                {
                    string full =
                        _absoluteUri.AbsoluteUri.Substring(0,
                        _absoluteUri.AbsoluteUri.LastIndexOf('/'));

                    return new Uri(full, UriKind.RelativeOrAbsolute);
                }
                else
                {
                    return _baseUri;
                }
            }
        }

        /// <summary>
        /// Gets the absolute URI.
        /// </summary>
        /// <value>The absolute URI.</value>
        public Uri AbsoluteUri
        {
            get
            {
                return _absoluteUri;
            }
        }

        /// <summary>
        /// Gets the type of the link.
        /// </summary>
        /// <value>The type of the link.</value>
        public UriType LinkType
        {
            get
            {
                if (_calculatedLinkType == null)
                {
                    _calculatedLinkType =
                        CheckVerifyLinkType(_absoluteUri, _linkType);
                }

                return _calculatedLinkType.Value;
            }
        }

        /// <summary>
        /// Decides whether to follow an URI.
        /// </summary>
        /// <value><c>true</c> if [want follow URI]; otherwise, <c>false</c>.</value>
        public bool WantFollowUri
        {
            get
            {
                return DoWantFollowUri(
                    _absoluteUri,
                    _linkType);
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is resource URI.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is resource URI; otherwise, <c>false</c>.
        /// </value>
        public bool IsResourceUri
        {
            get
            {
                return DoIsResourceUri(_absoluteUri);
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is processable URI.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is processable URI; otherwise, <c>false</c>.
        /// </value>
        public bool IsProcessableUri
        {
            get
            {
                return
                    !_originalUrl.StartsWith(@"#") &&
                    DoIsProcessableUri(_absoluteUri, _linkType);
            }
        }

        // ------------------------------------------------------------------
        #endregion

        #region Private methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Checks the type of the verify link.
        /// </summary>
        /// <param name="absoluteUri">The absolute URI.</param>
        /// <param name="linkType">Type of the link.</param>
        /// <returns></returns>
        private UriType CheckVerifyLinkType(
            Uri absoluteUri,
            UriType linkType)
        {
            if (linkType == UriType.Resource)
            {
                // The original.
                return linkType;
            }
            else
            {
                if (DoIsProcessableUri(absoluteUri, linkType))
                {
                    // Ensure PDFs don't get parsed.
                    string head = ResourceDownloader.DownloadHead(
                        absoluteUri,
                        _options);

                    if (string.IsNullOrEmpty(head))
                    {
                        return UriType.Resource;
                    }
                    else
                    {
                        head = head.ToLowerInvariant();

                        if (head.Contains(@"pdf") ||
                            head.Contains(@"application") ||
                            head.Contains(@"image"))
                        {
                            return UriType.Resource;
                        }
                        else
                        {
                            Debug.Assert(
                                head.Contains(@"text"),
                                @"no text document type but marked as content.");

                            // The original.
                            return linkType;
                        }
                    }
                }
                else
                {
                    return UriType.Resource;
                }
            }
        }

        /// <summary>
        /// Makes the absolute URI.
        /// </summary>
        /// <param name="baseUri">The base URI.</param>
        /// <param name="uri">The URI.</param>
        /// <returns></returns>
        private static Uri MakeAbsoluteUri(
            Uri baseUri,
            Uri uri)
        {
            if (Uri.IsWellFormedUriString(
                uri.OriginalString,
                UriKind.Absolute))
            {
                return uri;
            }
            else
            {
                if (baseUri == null)
                {
                    return uri;
                }
                else
                {
                    string[] up = uri.OriginalString.Split('?');

                    UriBuilder ub = new UriBuilder(baseUri.AbsoluteUri);
                    ub.Path = CombineVPath(ub.Path, up[0]);

                    if (up.Length > 1)
                    {
                        ub.Query = up[1];
                    }

                    return ub.Uri;
                }
            }
        }

        /// <summary>
        /// Combines the virtual path.
        /// </summary>
        /// <param name="s1">The s1.</param>
        /// <param name="s2">The s2.</param>
        /// <returns></returns>
        private static string CombineVPath(
            string s1,
            string s2)
        {
            if (string.IsNullOrEmpty(s1))
            {
                return s2;
            }
            else if (string.IsNullOrEmpty(s2))
            {
                return s1;
            }
            else
            {
                s1 = s1.TrimEnd('/');
                s2 = s2.TrimStart('/');

                return s1 + @"/" + s2;
            }
        }

        /// <summary>
        /// Cleanups the URL.
        /// </summary>
        /// <param name="url">The URL.</param>
        /// <returns></returns>
        private static string CleanupUrl(
            string url)
        {
            // Remove accidentially contained ASP-tags.
            url = Regex.Replace(
                url,
                @"<%.*?%>",
                string.Empty,
                RegexOptions.Singleline);

            // Remove anchors.
            url = Regex.Replace(
                url,
                @"#.*?$",
                string.Empty,
                RegexOptions.Singleline);

            return url;
        }

        /// <summary>
        /// Decides whether to follow an URI.
        /// </summary>
        /// <param name="absoluteUri">The absolute URI.</param>
        /// <param name="linkType">Type of the link.</param>
        /// <returns></returns>
        private static bool DoWantFollowUri(
            Uri absoluteUri,
            UriType linkType)
        {
            if (absoluteUri == null ||
                linkType == UriType.Resource)
            {
                return false;
            }
            else
            {
                if (absoluteUri.Scheme == Uri.UriSchemeHttp ||
                    absoluteUri.Scheme == Uri.UriSchemeHttps ||
                    absoluteUri.Scheme == Uri.UriSchemeFile)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Does the is resource URI.
        /// </summary>
        /// <param name="absoluteUri">The absolute URI.</param>
        /// <returns></returns>
        private static bool DoIsResourceUri(
            Uri absoluteUri)
        {
            return
                absoluteUri != null &&
                (
                absoluteUri.Scheme == Uri.UriSchemeHttp ||
                absoluteUri.Scheme == Uri.UriSchemeHttps ||
                absoluteUri.Scheme == Uri.UriSchemeFile);
        }

        /// <summary>
        /// Does the is processable URI.
        /// </summary>
        /// <param name="absoluteUri">The absolute URI.</param>
        /// <param name="linkType">Type of the link.</param>
        /// <returns></returns>
        private static bool DoIsProcessableUri(
            Uri absoluteUri,
            UriType linkType)
        {
            return
                absoluteUri != null &&
                (DoWantFollowUri(absoluteUri, linkType) ||
                DoIsResourceUri(absoluteUri));
        }

        // ------------------------------------------------------------------
        #endregion

        #region Private variables.
        // ------------------------------------------------------------------

        private readonly WebSiteDownloaderOptions _options;
        private readonly string _originalUrl;

        private readonly Uri _relativeUri = null;
        private readonly Uri _baseUri = null;
        private readonly Uri _absoluteUri = null;

        private readonly UriType _linkType = UriType.Content;
        private UriType? _calculatedLinkType = null;

        // ------------------------------------------------------------------
        #endregion
    }

    /// <summary>
    /// The type of an URI resource.
    /// </summary>
    public enum UriType
    {
        #region Enum members.
        // ------------------------------------------------------------------

        /// <summary>
        /// A HTML page with content to parse and to follow the links.
        /// </summary>
        Content,

        /// <summary>
        /// A resource link like images, videos, etc.
        /// </summary>
        Resource

        // ------------------------------------------------------------------
        #endregion
    }
}
